Auth0で保護されたGo gRPCサーバーをAWS上に構築してみる
調査する機会があったので、ブログにまとめてみました。
gRPCでは、データ通信にProtocol Buffersとよばれるシリアライゼーション形式を利用します。
Protocl Buffersは、データのシリアライゼーション形式として知られる一方で、シリアライズするためのスキーマ定義用として.proto
ファイルというインタフェース定義言語(IDL)を持っています。
実際の開発においては、この.proto
ファイルを先に作成することでスキーマファーストな進め方ができるのが大きな特徴です。
今回は、.proto
ファイルの作成~認可処理の実装~AWS環境へのデプロイまで一気通貫で紹介したいと思います。
せっかちな人へ
GitHubリポジトリにすべて上げています。
構成図
ディレクトリ構成
$ tree -L 2 . ├── Dockerfile ├── go.mod ├── go.sum ├── proto │ ├── gen │ └── helloworld.proto ├── README.md ├── server │ ├── auth │ ├── server.go │ └── server.go.back1 └── terraform ├── acm.tf ...
- proto
.proto
ファイルの保存先
- server
- サーバー側のソースコード保存先
- terraform
- AWSデプロイ用のTerraformのコード保存先
.protoファイルの作成
まず.proto
ファイルを作成していきます。
今回は、公式のサンプルを流用します。
- helloworld.protoファイルの作成
proto:helloworld.proto syntax = "proto3"; option go_package = "gen;gen"; option java_multiple_files = true; option java_package = "io.grpc.examples.helloworld"; option java_outer_classname = "HelloWorldProto"; package helloworld; // The greeting service definition. service Greeter { // Sends a greeting rpc SayHello (HelloRequest) returns (HelloReply) {} } // The request message containing the user's name. message HelloRequest { string name = 1; } // The response message containing the greetings message HelloReply { string message = 1; }
.proto
ファイルからインターフェースとなるGoのコードを自動作成
protoc -Iproto --go_out=plugins=grpc:proto proto/*.proto
gRPCサーバーを作成
gRPCサーバーのソースコードを書いていきます。
公式のサンプルを参考に進めます。
- Go version
$ go version go version go1.15.5 linux/amd64
- 初期化
go mod init helloworld
- モジュールの取得
go get -u google.golang.org/grpc
- コード
package main import ( "context" "log" "net" pb "helloworld/proto/gen" "google.golang.org/grpc" ) const ( port = ":50051" ) type server struct { pb.UnimplementedGreeterServer } func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { log.Printf("Received: %v", in.GetName()) return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil } func main() { lis, err := net.Listen("tcp", port) if err != nil { log.Fatalf("failed to listen: %v", err) } s := grpc.NewServer() pb.RegisterGreeterServer(s, &server{}) if err := s.Serve(lis); err != nil { log.Fatalf("failed to serve: %v", err) } }
ローカルサーバー上での動作確認
サーバーをローカルで起動し、grpcurlというツールを使って動作確認をしていきます。
- サーバー起動
go run server/server.go
- リクエストを送信
$ grpcurl \ -plaintext \ -d '{"name": "arai"}' \ -import-path proto \ -proto helloworld.proto \ localhost:50051 helloworld.Greeter/SayHello { "message": "Hello arai" }
Auth0にAPIを登録する
Auth0による認可機能を追加するため、Auth0ダッシュボードからAPIの登録を行っていきます。
- Auth0のダッシュボードよりAPIの作成
※ Identifier
にはAPIのエンドポイントURLを入力することが推奨されていますが、今回は適当に入力します。
- ClientIDを控えておきます
- アクセストークンも控えておきます
インターセプターに認可処理を追加する
Goのインターセプターの機能を使って、認可処理を実装していきます。
Auth0では、アクセストークンがjwt形式で提供されるためサーバー側で下記の処理が必要になります。(細かな点は公式ドキュメントを確認してください)
- 一般的なJWTの検証
aud
クレームがダッシュボードで指定したAPIのIdentifier
と一致しているか確認scope
クレームが設定されている場合は、適切なアクセスコントロールの処理
今回のケースでは、1と2をおこなう必要があります。
いい感じのモジュールがないかと調べましたが、結論から言うと本番では自前実装するの必要がありそうです。
今回は簡易的に、Auth0 Communityの方で作成されているauth0-goモジュールを利用しますが、Issueにもある通り今後メンテナンスする予定はなさそうです。
※ 本番利用を検討する際は、auth0-goやgo-jwt-middlewareのコードを参考に開発するのが良さそうです。
- モジュールの取得
go get -u github.com/grpc-ecosystem/go-grpc-middleware go get github.com/auth0-community/go-auth0@b9b0f95be5688e006bed4a55a7aae9145cfc0370 # ログ用にzapも入れておく go get -u go.uber.org/zap
- コード修正
package main import ( "context" "log" "net" pb "helloworld/proto/gen" "go.uber.org/zap" "google.golang.org/grpc" "google.golang.org/grpc/codes" "gopkg.in/square/go-jose.v2" "gopkg.in/square/go-jose.v2/jwt" auth0 "github.com/auth0-community/go-auth0" grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" grpc_auth "github.com/grpc-ecosystem/go-grpc-middleware/auth" grpc_zap "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap" ) const ( port = ":50051" // TODO: fill auth0_url = "<your_auth0_url>" audience = "<your_audience>" ) type server struct { pb.UnimplementedGreeterServer } func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { log.Printf("Received: %v", in.GetName()) return &pb.HelloReply{Message: "Hello " + in.GetName() + " your id is " + ctx.Value("userId").(string)}, nil } func auth(ctx context.Context) (context.Context, error) { token, err := grpc_auth.AuthFromMD(ctx, "Bearer") if err != nil { return nil, err } parsedToken, err := jwt.ParseSigned(token) if err != nil { return nil, grpc.Errorf(codes.Unauthenticated, "Cannot parse token because of", err) } client := auth0.NewJWKClient(auth0.JWKClientOptions{URI: "https://" + auth0_url + "/.well-known/jwks.json"}, nil) configuration := auth0.NewConfiguration(client, []string{audience}, "https://"+auth0_url+"/", jose.RS256) validator := auth0.NewValidator(configuration, nil) if err := validator.ValidateToken(parsedToken); err != nil { return nil, grpc.Errorf(codes.Unauthenticated, "Cannot validate token because of", err) } claims := make(map[string]interface{}) validator.Claims(parsedToken, &claims) return context.WithValue(ctx, "userId", claims["sub"]), nil } func main() { lis, err := net.Listen("tcp", port) if err != nil { log.Fatalf("failed to listen: %v", err) } zapLogger, err := zap.NewProduction() if err != nil { panic(err) } grpc_zap.ReplaceGrpcLogger(zapLogger) s := grpc.NewServer( grpc.UnaryInterceptor( grpc_middleware.ChainUnaryServer( grpc_zap.UnaryServerInterceptor(zapLogger), grpc_auth.UnaryServerInterceptor(auth), ), ), ) pb.RegisterGreeterServer(s, &server{}) if err := s.Serve(lis); err != nil { log.Fatalf("failed to serve: %v", err) } }
※ auth0_url
にはxxx.auth0.com
のようなテナントURL、audience
には先ほど控えたAPIのIdentifier
を入力してください。
Dockerで起動
今度はDockerコンテナ上で起動し、動作確認していきます。
- Dockerfileの作成
FROM golang:1.15.5 as builder ENV CGO_ENABLED=0 GOOS=linux GOARCH=amd64 WORKDIR /build COPY . . WORKDIR /build/server RUN go build FROM alpine:3.12.1 COPY --from=builder /build/server/server /opt/app/ ENTRYPOINT [ "/opt/app/server" ]
docker build -t go-grpc-server -f Dockerfile . docker run --rm --name go-grpc -p 50051:50051 go-grpc-server
- 動作確認
$ grpcurl \ -plaintext \ -H "Authorization: Bearer <your-access-token>" \ -d '{"name": "arai"}' \ -import-path proto \ -proto helloworld.proto \ localhost:50051 helloworld.Greeter/SayHello { "message": "Hello arai your id is xxxxxxxxxx@clients" }
※ <your-access-token>
は先ほど控えておいたアクセストークンに変えてください
AWSリソースの作成
Terraformを使ってAWSリソースを作成していきます。
本筋とは関係ないため、細かい部分は省きます。気になる方はGitHubリポジトリのREADMEを見てください。
イメージのプッシュ
docker-push.sh
を作成し実行
#!/usr/bin/env bash # Version=latest ACCOUNT_ID=$(aws sts get-caller-identity --output text --query 'Account') ECR_PREFIX="${ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com" # echo $Version echo $ACCOUNT_ID aws ecr get-login-password | docker login --username AWS --password-stdin $ACCOUNT_ID.dkr.ecr.ap-northeast-1.amazonaws.com # go grpc server docker tag go-grpc-server:latest ${ECR_PREFIX}/go-grpc-server-test-ecr-repo/go-grpc-server:latest docker push ${ECR_PREFIX}/go-grpc-server-test-ecr-repo/go-grpc-server:latest
AWS上での動作確認
- 動作確認
$ grpcurl \ -H "Authorization: Bearer ${access_token}" \ -d '{"name": "arai"}' \ -import-path proto \ -proto helloworld.proto \ go-grpc-server.cm-arai.com:50051 helloworld.Greeter/SayHello { "message": "Hello arai your id is xxxxxxxxxx@clients" }
- ログ
まとめ
いかがだったでしょうか。
どなたかの役にたてば幸いです。